集合将自己持有的数据存储在堆上。
不同的集合类型有着不同的性能特性与开销。
常见的集合类型:
- 动态数组:紧密地存储任意多个值;
- 字符串:字符的集合,例如 String 类型;
- 哈希映射:将值关联到一个特定的值上,是映射的特殊实现;
动态数组
动态数组类型 Vec<T>,在单个数据结构中存储多个类型相同的值,这些值会彼此相邻地排布在内存中。
动态数组非常适合在需要存储一系列相同类型值的场景中使用。
创建动态数组
let v: Vec<i32> = Vec::new();
以上显式地增加一个类型标记,因为没插入任何值,Rust 无法自动推导出我们想要存储的元素类型。动态数组在实现中使用了泛型。
在实际编码中,只要向动态数组内插入了数据,Rust 便可以在绝大部分情形下推导出你希望存储的元素类型,你只需要在极少数的场景中对类型进行声明。
使用初始值来创建动态数组,Rust 提供了一个宏 (vec!) 来创建
let v = vec![1, 2, 3];
修改数据
可以使用 push 方法向动态数组中添加数据
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
读取数据
有两种方法可以引用存储在动态数组中的值:
- 索引;
- get() 方法;
示例代码
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let four: Option<&i32> = v.get(3);
match four {
Some(four) => println!("The four element is {four}"),
None => println!("There is no four element."),
}
}
get() 方法会返回一个 Option<&T> 类型,可进一步对它进行匹配操作。
遍历数据
示例代码
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
遍历的同时修改数据
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
枚举与动态数组结合
动态数组只能存储同一类型的值,为了能让动态数据能够存储不同类型的值,可与枚举结合来使用
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(10.12),
SpreadsheetCell::Text(String::from("blue")),
];
字符串与哈希映射
说明都在以下代码注释中展示
// 集合类型:
// 1.动态数组
// 2.字符串
// 3.哈希映射
use std::fmt;
use std::collections::HashMap;
// 通过枚举类型使得动态数组能存储多个类型的值
// #[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
// 自己手动为 SpreadsheetCell 实现 Debug trait
impl fmt::Debug for SpreadsheetCell {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SpreadsheetCell::Int(value) => write!(f, "Int({})", value),
SpreadsheetCell::Float(value) => write!(f, "Float({})", value),
SpreadsheetCell::Text(value) => write!(f, "Text({})", value),
}
}
}
fn main() {
// ================================================
// 第一种集合类型:动态数组 Vec<T>
// ================================================
// 创建动态数组 - 紧密地存储任意多个值
// 动态数组在实现中使用了泛型
// v 变量绑定的 Vec<T> 持有 i32 类型的元素
let mut v1: Vec<i32> = Vec::new();
// 更新动态数组
// 在更新动态数组前,需将动态数组声明为可变 mut
v1.push(5);
v1.push(6);
v1.push(7);
v1.push(8);
for i in &mut v1 {
// 通过解引用来获取 i 的值并修改它
*i += 50;
println!("{i}");
}
// Rust 特意提供了一个用于简化代码的 vec! 宏
// 这个宏可以根据提供的值来创建一个动态数组
let v2 = vec![1, 2, 3, 4, 5];
// 通过索引方式来访问动态数组内容
// third 不可变引用
let third: &i32 = &v2[2];
print!("The third element is {third}");
// 使用 get() 方法来访问动态数组中的元素
// 好处是当越界访问时不会 panic,而是返回 None
let four: Option<&i32> = v2.get(3);
match four {
Some(four) => println!("The four element is {four}"),
None => println!("There is no four element."),
}
// 通过枚举类型使得动态数组能存储多个类型的值
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(10.12),
SpreadsheetCell::Text(String::from("blue")),
];
println!("{:#?}", row);
// ================================================
// 第二种集合类型:字符串
// ================================================
// 字符串类型;字符串切片 str,常以借用形式 &str 出现
// 基于 UTF-8 编码
// 创建新的字符串
let s1 = String::new();
// 很多适用于 Vec<T> 的方法同样也适用于 String
// 因为 String 是基于字节动态数组实现的
let s2 = "initial contents";
let s3 = s2.to_string();
// 以上等价于 let s3 = String::from("initial contents");
println!("{s1} - {s3}");
// 更新字符串
// 使用 +、format! 宏、push_str(字符串切片)、push(单个字符)
let mut s4 = String::from("foo");
s4.push_str("bar");
println!("s4 = {s4}");
let mut s5 = String::from("lo");
s5.push('l');
println!("s5 = {s5}");
let s6 = String::from("Hello, ");
let s7 = String::from("World!");
// + 运算符会调用一个 add 方法
// 类似 fn add(self, s: &str) -> String,与标准库有些许差别
// add 函数使用了泛型来定义
// 只能将 String 类型与 &str 相加,而不能将两个 String 类型的值相加
// 这里 &s7,即 &String,而不是 &str,编译器会自动将其进行转换,称为解引用转换技术
// add 不会取得 s 的所有权,所以调用后再去使用 s 是可以的
let s8 = s6 + &s7; // s6 在此处所有权发生变化
// s6 不可用
// println!("s6 = {s6}"); // 报错
// 而 s7 不会,因为引用
println!("s8 = {s8}, s7 = {s7}");
// 拼接多个字符串,内部实现原理可以说是很复杂
let s9 = String::from("tic");
let s10 = String::from("tac");
let s11 = String::from("toe");
let s12 = s9 + "-" + &s10 + "-" + &s11;
println!("s12 = {s12}");
// 针对以上多个字符串合并,Rust 提供了 format! 宏
// format! 宏会将结果包含在一个 String 中返回
// 这样更加易读,并且不会夺取任何参数的所有权,因为 format! 生成的代码使用了引用
let s13 = String::from("tic");
let s14 = format!("{s13}-{s10}-{s11}");
println!("s14 = {s14}");
// 索引字符串,Rust 不像其它语言那样可以通过 [index] 的方式来索引字符串
// 因为是 UTF-8 编码的
// 要小心谨慎地使用范围语法来创建字符串切片
let s15 = "процессоре";
let s16 = &s15[0..4];
println!("s16 = {s16}");
// 遍历字符串的方法
for c in "цес".chars() {
println!("{c}");
}
for b in "цес".bytes() {
println!("{b}");
}
// ================================================
// 第三种集合类型:哈希映射 HashMap<K, V>
// ================================================
// 内部实现是使用了哈希函数
// 创建一个哈希映射
let mut scores = HashMap::new();
// 如果 Blue 与 Yellow 另外声明的话,例如:let field_name = String::from("Blue")
// 那么在 insert 后,由于所有权转移到了哈希映射中,field_name 也就失效了
// 后续再使用 field_name 变量将导致编译发生错误
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 访问哈希映射中的值
let team_name = String::from("Blue");
// get 会返回一个 Option<&V>
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("score = {score}");
// 使用 for 循环遍历哈希映射中所有的键值对
for (key, value) in &scores {
println!("{key}: {value}");
}
// 更新哈希映射
// 以下将原来的值给覆盖掉了
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
// entry() 检测键是否存在,如果不存在,则将参数作为值插入
scores.entry(String::from("Yellow")).or_insert(60);
scores.entry(String::from("Green")).or_insert(80);
println!("{:?}", scores);
// 基于旧值来更新值
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}
